// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:analyzer/src/dart/sdk/sdk.dart';
import 'package:analyzer/src/test_utilities/mock_sdk.dart';
import 'package:dartdoc/src/dartdoc_options.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/package_config_provider.dart';
import 'package:dartdoc/src/package_meta.dart';
import 'package:dartdoc/src/warnings.dart';
import 'package:test/test.dart';

import 'src/utils.dart' as utils;

void main() {
  MemoryResourceProvider resourceProvider;
  PackageMetaProvider packageMetaProvider;
  FakePackageConfigProvider packageConfigProvider;
  Folder projectRoot;
  String projectPath;
  var packageName = 'my_package';
  PackageGraph packageGraph;
  ModelElement libraryModel;

  Matcher hasInvalidParameterWarning(String message) =>
      _HasWarning(PackageWarning.invalidParameter, message);

  Matcher hasMissingExampleWarning(String message) =>
      _HasWarning(PackageWarning.missingExampleFile, message);

  void expectNoWarnings() {
    expect(packageGraph.packageWarningCounter.hasWarnings, isFalse);
    expect(packageGraph.packageWarningCounter.countedWarnings, isEmpty);
  }

  group('documentation_comment tests', () {
    setUp(() async {
      resourceProvider = MemoryResourceProvider();
      final sdkRoot = resourceProvider.getFolder(
        resourceProvider.convertPath('/sdk'),
      );
      createMockSdk(
        resourceProvider: resourceProvider,
        root: sdkRoot,
      );
      utils.writeMockSdkFiles(sdkRoot);

      packageMetaProvider = PackageMetaProvider(
          PubPackageMeta.fromElement,
          PubPackageMeta.fromFilename,
          PubPackageMeta.fromDir,
          resourceProvider,
          sdkRoot,
          defaultSdk: FolderBasedDartSdk(resourceProvider, sdkRoot),
          messageForMissingPackageMeta:
              PubPackageMeta.messageForMissingPackageMeta);
      var optionSet = await DartdocOptionSet.fromOptionGenerators(
          'dartdoc', [createDartdocOptions], packageMetaProvider);
      optionSet.parseArguments([]);
      packageConfigProvider = FakePackageConfigProvider();
      // To build the package graph, we always ask package_config for a
      // [PackageConfig] for the SDK directory. Put a dummy entry in.
      packageConfigProvider.addPackageToConfigFor(
          sdkRoot.path, 'analyzer', Uri.file('/sdk/pkg/analyzer/'));

      projectRoot = utils.writePackage(
          packageName, resourceProvider, packageConfigProvider);
      projectPath = projectRoot.path;
      projectRoot
          .getChildAssumingFolder('lib')
          .getChildAssumingFile('a.dart')
          .writeAsStringSync('''
/// Documentation comment.
int x;
''');
      packageGraph = await utils.bootBasicPackage(
          projectPath, packageMetaProvider, packageConfigProvider,
          additionalArguments: []);
      libraryModel = packageGraph.defaultPackage.libraries.first;
    });

    test('removes triple slashes', () async {
      var doc = await libraryModel.processComment('''
/// Text.
/// More text.
''');

      expect(doc, equals('''
Text.
More text.'''));
    });

    test('removes space after triple slashes', () async {
      var doc = await libraryModel.processComment('''
///  Text.
///    More text.
''');

      // TODO(srawlins): Actually, the three spaces before 'More' is perhaps not
      // the best fit. Should it only be two, to match the indent from the first
      // line's "Text"?
      expect(doc, equals('''
Text.
   More text.'''));
    });

    test('leaves blank lines', () async {
      var doc = await libraryModel.processComment('''
/// Text.
///
/// More text.
''');

      expect(doc, equals('''
Text.

More text.'''));
    });

    test('warns when an unknown directive is parsed', () async {
      await libraryModel.processComment('''
/// Text.
///
/// {@marco name}
''');
      expect(
          packageGraph.packageWarningCounter.hasWarning(
              libraryModel, PackageWarning.unknownDirective, "'marco'"),
          isTrue);
    });

    test('warns when a directive with wrong case is parsed', () async {
      await libraryModel.processComment('''
/// Text.
///
/// {@youTube url}
''');
      expect(
          packageGraph.packageWarningCounter.hasWarning(libraryModel,
              PackageWarning.unknownDirective, "'youTube' (use lowercase)"),
          isTrue);
    });

    test('processes @animation', () async {
      var doc = await libraryModel.processComment('''
/// Text.
///
/// {@animation 100 200 http://host/path/to/video.mp4 id=barHerderAnimation}
///
/// End text.
''');

      expectNoWarnings();
      var rendered = libraryModel.modelElementRenderer.renderAnimation(
          'barHerderAnimation',
          100,
          200,
          Uri.parse('http://host/path/to/video.mp4'),
          'barHerderAnimation_play_button_');
      expect(doc, equals('''
Text.

$rendered

End text.'''));
    });

    test('renders an unnamed @animation', () async {
      var doc = await libraryModel.processComment('''
/// First line.
///
/// {@animation 100 200 http://host/path/to/video.mp4}
''');

      expectNoWarnings();
      expect(doc, contains('<video id="animation_1"'));
    });

    test('renders a named @animation', () async {
      var doc = await libraryModel.processComment('''
/// First line.
///
/// {@animation 100 200 http://host/path/to/video.mp4 id=namedAnimation}
''');

      expectNoWarnings();
      expect(doc, contains('<video id="namedAnimation"'));
    });

    test('renders a named @animation, out-of-order', () async {
      var doc = await libraryModel.processComment('''
/// First line.
///
/// {@animation 100 200 id=namedAnimation http://host/path/to/video.mp4}
''');

      expectNoWarnings();
      expect(doc, contains('<video id="namedAnimation"'));
    });

    test('renders a named @animation with double quotes', () async {
      var doc = await libraryModel.processComment('''
/// First line.
///
/// {@animation 100 200 http://host/path/to/video.mp4 id="namedAnimation"}
''');

      expectNoWarnings();
      expect(doc, contains('<video id="namedAnimation"'));
    });

    test('renders a named @animation with single quotes', () async {
      var doc = await libraryModel.processComment('''
/// First line.
///
/// {@animation 100 200 http://host/path/to/video.mp4 id='namedAnimation'}
''');

      expectNoWarnings();
      expect(doc, contains('<video id="namedAnimation"'));
    });

    test('renders multiple @animation using unique IDs', () async {
      var doc = await libraryModel.processComment('''
/// First line.
///
/// {@animation 100 200 http://host/path/to/video.mp4}
/// {@animation 100 200 http://host/path/to/video2.mp4}
''');

      expectNoWarnings();
      expect(doc, contains('<video id="animation_1"'));
      expect(doc, contains('<video id="animation_2"'));

      // A second element with unnamed animations requires unique IDs as well.
      doc = await libraryModel.processComment('''
/// First line.
///
/// {@animation 100 200 http://host/path/to/video.mp4}
/// {@animation 100 200 http://host/path/to/video2.mp4}
''');

      expectNoWarnings();
      expect(doc, contains('<video id="animation_3"'));
      expect(doc, contains('<video id="animation_4"'));
    });

    test('warns when @animation has fewer than 3 arguments', () async {
      await libraryModel.processComment('''
/// Text.
///
/// {@animation 100 http://host/path/to/video.mp4 id=barHerderAnimation}
///
/// End text.
''');

      expect(
          libraryModel,
          hasInvalidParameterWarning(
              'Invalid @animation directive, "{@animation 100 '
              'http://host/path/to/video.mp4 id=barHerderAnimation}"\n'
              'Animation directives must be of the form "{@animation WIDTH '
              'HEIGHT URL [id=ID]}"'));
    });

    test('warns when @animation has more than 4 arguments', () async {
      await libraryModel.processComment('''
/// Text.
///
/// {@animation 100 200 300 400 http://host/path/to/video.mp4 id=barHerderAnimation}
///
/// End text.
''');

      expect(
          libraryModel,
          hasInvalidParameterWarning(
              'Invalid @animation directive, "{@animation 100 200 300 400 '
              'http://host/path/to/video.mp4 id=barHerderAnimation}"\n'
              'Animation directives must be of the form "{@animation WIDTH '
              'HEIGHT URL [id=ID]}"'));
    });

    test('warns when @animation has more than 4 arguments', () async {
      await libraryModel.processComment('''
/// Text.
///
/// {@animation 100 200 300 400 http://host/path/to/video.mp4 id=barHerderAnimation}
///
/// End text.
''');

      expect(
          libraryModel,
          hasInvalidParameterWarning(
              'Invalid @animation directive, "{@animation 100 200 300 400 '
              'http://host/path/to/video.mp4 id=barHerderAnimation}"\n'
              'Animation directives must be of the form "{@animation WIDTH '
              'HEIGHT URL [id=ID]}"'));
    });

    test('warns when @animation has a non-unique identifier', () async {
      await libraryModel.processComment('''
/// Text.
///
/// {@animation 100 200 http://host/path/to/video.mp4 id=barHerderAnimation}
/// {@animation 100 200 http://host/path/to/video.mpg id=barHerderAnimation}
///
/// End text.
''');

      expect(
          libraryModel,
          hasInvalidParameterWarning(
              'An animation has a non-unique identifier, '
              '"barHerderAnimation". Animation identifiers must be unique.'));
    });

    test('warns when @animation has an invalid identifier', () async {
      await libraryModel.processComment('''
/// Text.
///
/// {@animation 100 200 http://host/path/to/video.mp4 id=not-valid}
///
/// End text.
''');

      expect(
          libraryModel,
          hasInvalidParameterWarning(
              'An animation has an invalid identifier, "not-valid". The '
              'identifier can only contain letters, numbers and underscores, and '
              'must not begin with a number.'));
    });

    test('warns when @animation has a malformed width', () async {
      await libraryModel.processComment('''
/// Text.
///
/// {@animation 100px 200 http://host/path/to/video.mp4 id=barHerderAnimation}
///
/// End text.
''');

      expect(
          libraryModel,
          hasInvalidParameterWarning(
              'An animation has an invalid width (barHerderAnimation), '
              '"100px". The width must be an integer.'));
    });

    test('warns when @animation has a malformed height', () async {
      await libraryModel.processComment('''
/// Text.
///
/// {@animation 100 200px http://host/path/to/video.mp4 id=barHerderAnimation}
///
/// End text.
''');

      expect(
          libraryModel,
          hasInvalidParameterWarning(
              'An animation has an invalid height (barHerderAnimation), '
              '"200px". The height must be an integer.'));
    });

    test('warns when @animation has an unknown parameter', () async {
      await libraryModel.processComment('''
/// Text.
///
/// {@animation 100 200 http://host/path/to/video.mp4 name=barHerderAnimation}
///
/// End text.
''');

      expect(
          libraryModel,
          hasInvalidParameterWarning(
              'The {@animation ...} directive was called with invalid '
              'parameters. FormatException: Could not find an option named '
              '"name".'));
    });

    test('warns when @animation uses the deprecated syntax', () async {
      await libraryModel.processComment('''
/// Text.
///
/// {@animation barHerderAnimation 100 200 http://host/path/to/video.mp4}
///
/// End text.
''');

      expect(
          packageGraph.packageWarningCounter.hasWarning(
              libraryModel,
              PackageWarning.deprecated,
              'Deprecated form of @animation directive, "{@animation '
              'barHerderAnimation 100 200 http://host/path/to/video.mp4}"\n'
              'Animation directives are now of the form "{@animation WIDTH '
              'HEIGHT URL [id=ID]}" (id is an optional parameter)'),
          isTrue);
    });

    test('processes @template', () async {
      var doc = await libraryModel.processComment('''
/// Text.
///
/// {@template abc}
/// Template text.
/// {@endtemplate}
///
/// End text.
''');

      expectNoWarnings();
      expect(doc, equals('''
Text.

{@macro abc}

End text.'''));
    });

    test('processes leading @template', () async {
      var doc = await libraryModel.processComment('''
/// {@template abc}
/// Template text.
/// {@endtemplate}
///
/// End text.
''');

      expectNoWarnings();
      expect(doc, equals('''
{@macro abc}

End text.'''));
    });

    test('processes trailing @template', () async {
      var doc = await libraryModel.processComment('''
/// Text.
///
/// {@template abc}
/// Template text.
/// {@endtemplate}
''');

      expectNoWarnings();
      expect(doc, equals('''
Text.

{@macro abc}'''));
    });

    test('processes @template w/o blank line following', () async {
      var doc = await libraryModel.processComment('''
/// Text.
///
/// {@template abc}
/// Template text.
/// {@endtemplate}
/// End text.
''');

      expectNoWarnings();
      expect(doc, equals('''
Text.

{@macro abc}
End text.'''));
    });

    test('allows whitespace around @template name', () async {
      var doc = await libraryModel.processComment('''
/// {@template    abc    }
/// Template text.
/// {@endtemplate}
''');

      expectNoWarnings();
      expect(doc, equals('{@macro abc}'));
    });

    test('processes @example with file', () async {
      projectRoot.getChildAssumingFile('abc.md').writeAsStringSync('''
```plaintext
Code snippet
```
''');
      var doc = await libraryModel.processComment('''
/// Text.
///
/// {@example abc}
///
/// End text.
''');

      expectNoWarnings();
      expect(doc, equals('''
Text.

```plaintext
Code snippet
```


End text.'''));
    });

    test('processes @example with a region', () async {
      projectRoot
          .getChildAssumingFile('abc-r.md')
          .writeAsStringSync('Markdown text.');
      var doc = await libraryModel.processComment('''
/// Text.
///
/// {@example region=r abc}
''');

      expectNoWarnings();
      expect(doc, equals('''
Text.

Markdown text.'''));
    });

    test('adds language to processed @example with an extension and no lang',
        () async {
      projectRoot.getChildAssumingFile('abc.html.md').writeAsStringSync('''
```
Code snippet
```
''');
      var doc = await libraryModel.processComment('''
/// Text.
///
/// {@example abc.html}
///
/// End text.
''');

      expectNoWarnings();
      expect(doc, equals('''
Text.

```html
Code snippet
```


End text.'''));
    });

    test('adds language to processed @example with a lang and an extension',
        () async {
      projectRoot.getChildAssumingFile('abc.html.md').writeAsStringSync('''
```
Code snippet
```
''');
      var doc = await libraryModel.processComment('''
/// Text.
///
/// {@example abc.html lang=html}
''');

      expectNoWarnings();
      expect(doc, equals('''
Text.

```html
Code snippet
```
'''));
    });

    test('adds language to processed @example with a lang and no extension',
        () async {
      projectRoot.getChildAssumingFile('abc.md').writeAsStringSync('''
```
Code snippet
```
''');
      var doc = await libraryModel.processComment('''
/// Text.
///
/// {@example abc lang=html}
''');

      expectNoWarnings();
      expect(doc, equals('''
Text.

```html
Code snippet
```
'''));
    });

    test('processes @example with file, not found', () async {
      var doc = await libraryModel.processComment('''
/// {@example abc}
''');

      var abcPath = resourceProvider.pathContext.canonicalize(
          resourceProvider.pathContext.join(projectRoot.path, 'abc.md'));
      var libPathInWarning = resourceProvider.pathContext.join('lib', 'a.dart');
      expect(
          libraryModel,
          hasMissingExampleWarning(
              '$abcPath; path listed at $libPathInWarning'));
      // When the example path is invalid, the directive should be left in-place.
      expect(doc, equals('{@example abc}'));
    });

    test('processes @example with directories, not found', () async {
      var doc = await libraryModel.processComment('''
/// {@example abc/def/ghi}
''');
      var abcPath = resourceProvider.pathContext.canonicalize(resourceProvider
          .pathContext
          .join(projectRoot.path, 'abc', 'def', 'ghi.md'));
      var libPathInWarning = resourceProvider.pathContext.join('lib', 'a.dart');
      expect(
          libraryModel,
          hasMissingExampleWarning(
              '$abcPath; path listed at $libPathInWarning'));
      // When the example path is invalid, the directive should be left in-place.
      expect(doc, equals('{@example abc/def/ghi}'));
    });

    test('processes @example with a region, not found', () async {
      var doc = await libraryModel.processComment('''
/// {@example region=r abc}
''');
      var abcPath = resourceProvider.pathContext.canonicalize(
          resourceProvider.pathContext.join(projectRoot.path, 'abc-r.md'));
      var libPathInWarning = resourceProvider.pathContext.join('lib', 'a.dart');
      expect(
          libraryModel,
          hasMissingExampleWarning(
              '$abcPath; path listed at $libPathInWarning'));
      // When the example path is invalid, the directive should be left in-place.
      expect(doc, equals('{@example region=r abc}'));
    });

    test('leaves @inject-html unprocessed when disabled', () async {
      var doc = await libraryModel.processComment('''
/// Text.
///
/// {@inject-html}<script></script>{@end-inject-html}
''');

      expectNoWarnings();
      expect(doc, equals('''
Text.

{@inject-html}<script></script>{@end-inject-html}'''));
    });

    test('leaves @tool unprocessed when disabled', () async {
      var doc = await libraryModel.processComment('''
/// Text.
///
/// {@tool date}{@end-tool}
''');

      expectNoWarnings();
      expect(doc, equals('''
Text.

{@tool date}{@end-tool}'''));
    });

    test('processes @inject-html when enabled', () async {
      packageGraph = await utils.bootBasicPackage(
          projectPath, packageMetaProvider, packageConfigProvider,
          additionalArguments: ['--inject-html']);
      libraryModel = packageGraph.defaultPackage.libraries.first;
      var doc = await libraryModel.processComment('''
/// Text.
///
/// {@inject-html}<script></script>{@end-inject-html}
''');

      expectNoWarnings();
      expect(doc, equals('''
Text.


<dartdoc-html>6829def5ec06d211fa90fe69a58213ae901f3ee4</dartdoc-html>
'''));
    });

    test('processes @youtube', () async {
      var doc = await libraryModel.processComment('''
/// Text.
///
/// {@youtube 100 200 https://www.youtube.com/watch?v=oHg5SJYRHA0}
///
/// End text.
''');

      expectNoWarnings();
      expect(
          doc,
          matches(RegExp(
              '^Text.\n\n+'
              r'<p style="position: relative;\s+padding-top: 200.00%;">\s*'
              r'<iframe src="https://www.youtube.com/embed/oHg5SJYRHA0\?rel=0".*</iframe>\s*'
              '</p>\n\n+'
              r'End text.$',
              multiLine: true,
              dotAll: true)));
    });

    test('processes leading @youtube', () async {
      var doc = await libraryModel.processComment('''
/// {@youtube 100 200 https://www.youtube.com/watch?v=oHg5SJYRHA0}
///
/// End text.
''');

      expectNoWarnings();
      expect(
          doc,
          matches(RegExp(
              r'^<p style="position: relative;\s+padding-top: 200.00%;">\s*'
              r'<iframe src="https://www.youtube.com/embed/oHg5SJYRHA0\?rel=0".*</iframe>\s*'
              '</p>\n\n+'
              r'End text.$',
              multiLine: true,
              dotAll: true)));
    });

    test('processes trailing @youtube', () async {
      var doc = await libraryModel.processComment('''
/// Text.
///
/// {@youtube 100 200 https://www.youtube.com/watch?v=oHg5SJYRHA0}
''');

      expectNoWarnings();
      expect(
          doc,
          matches(RegExp(
              '^Text.\n\n+'
              r'<p style="position: relative;\s+padding-top: 200.00%;">\s*'
              r'<iframe src="https://www.youtube.com/embed/oHg5SJYRHA0\?rel=0".*</iframe>\s*'
              r'</p>$',
              multiLine: true,
              dotAll: true)));
    });

    test('warns when @youtube has less than 3 arguments', () async {
      await libraryModel.processComment(
          '/// {@youtube 100 https://www.youtube.com/watch?v=oHg5SJYRHA0}');

      expect(
          libraryModel,
          hasInvalidParameterWarning('Invalid @youtube directive, '
              '"{@youtube 100 https://www.youtube.com/watch?v=oHg5SJYRHA0}"\n'
              'YouTube directives must be of the form '
              '"{@youtube WIDTH HEIGHT URL}"'));
    });

    test('warns when @youtube has more than 3 arguments', () async {
      await libraryModel.processComment(
          '/// {@youtube 100 200 300 https://www.youtube.com/watch?v=oHg5SJYRHA0}');

      expect(
          libraryModel,
          hasInvalidParameterWarning('Invalid @youtube directive, '
              '"{@youtube 100 200 300 https://www.youtube.com/watch?v=oHg5SJYRHA0}"\n'
              'YouTube directives must be of the form '
              '"{@youtube WIDTH HEIGHT URL}"'));
    });

    test('warns when @youtube has a malformed width', () async {
      await libraryModel.processComment(
          '/// {@youtube 100px 200 https://www.youtube.com/watch?v=oHg5SJYRHA0}');

      expect(
          libraryModel,
          hasInvalidParameterWarning(
              'A @youtube directive has an invalid width, "100px". '
              'The width must be a positive integer.'));
    });

    test('warns when @youtube has a negative width', () async {
      await libraryModel.processComment(
          '/// {@youtube -100 200 https://www.youtube.com/watch?v=oHg5SJYRHA0}');

      expect(
          libraryModel,
          hasInvalidParameterWarning(
              'The {@youtube ...} directive was called with invalid '
              'parameters. FormatException: Could not find an option with '
              'short name "-1".'));
    });

    test('warns when @youtube has a malformed height', () async {
      await libraryModel.processComment(
          '/// {@youtube 100 200px https://www.youtube.com/watch?v=oHg5SJYRHA0}');

      expect(
          libraryModel,
          hasInvalidParameterWarning(
              'A @youtube directive has an invalid height, "200px". The height '
              'must be a positive integer.'));
    });

    test('warns when @youtube has a negative height', () async {
      await libraryModel.processComment(
          '/// {@youtube 100 -200 https://www.youtube.com/watch?v=oHg5SJYRHA0}');

      expect(
          libraryModel,
          hasInvalidParameterWarning(
              'The {@youtube ...} directive was called with invalid '
              'parameters. FormatException: Could not find an option with '
              'short name "-2".'));
    });

    test('warns when @youtube has an invalid URL', () async {
      await libraryModel.processComment(
          '/// {@youtube 100 200 https://www.not-youtube.com/watch?v=oHg5SJYRHA0}');

      expect(
          libraryModel,
          hasInvalidParameterWarning('A @youtube directive has an invalid URL: '
              '"https://www.not-youtube.com/watch?v=oHg5SJYRHA0". Supported '
              'YouTube URLs have the following format: '
              'https://www.youtube.com/watch?v=oHg5SJYRHA0.'));
    });

    test('warns when @youtube has a URL with extra query parameters', () async {
      await libraryModel.processComment(
          '/// {@youtube 100 200 https://www.not-youtube.com/watch?v=oHg5SJYRHA0&a=1}');

      expect(
          libraryModel,
          hasInvalidParameterWarning('A @youtube directive has an invalid URL: '
              '"https://www.not-youtube.com/watch?v=oHg5SJYRHA0&a=1". '
              'Supported YouTube URLs have the following format: '
              'https://www.youtube.com/watch?v=oHg5SJYRHA0.'));
    });

    test('warns when fenced code block does not specify language', () async {
      await libraryModel.processComment('''
/// ```
/// void main() {}
/// ```
''');

      expect(
          packageGraph.packageWarningCounter.hasWarning(
              libraryModel,
              PackageWarning.missingCodeBlockLanguage,
              'A fenced code block in Markdown should have a language specified'),
          isTrue);
    });

    test('warns when squiggly fenced code block does not specify language',
        () async {
      await libraryModel.processComment('''
/// ~~~
/// void main() {}
/// ~~~
''');

      expect(
          packageGraph.packageWarningCounter.hasWarning(
              libraryModel,
              PackageWarning.missingCodeBlockLanguage,
              'A fenced code block in Markdown should have a language specified'),
          isTrue);
    });

    test('does not warn when fenced code block does specify language',
        () async {
      await libraryModel.processComment('''
/// ```dart
/// void main() {}
/// ```
''');

      expect(
          packageGraph.packageWarningCounter.hasWarning(
              libraryModel,
              PackageWarning.missingCodeBlockLanguage,
              'A fenced code block in Markdown should have a language specified'),
          isFalse);
    });

    test('does not warn when fenced block is not closed', () async {
      await libraryModel.processComment('''
/// ```
/// A not closed fenced code block
''');

      expect(
          packageGraph.packageWarningCounter.hasWarning(
              libraryModel,
              PackageWarning.missingCodeBlockLanguage,
              'A fenced code block in Markdown should have a language specified'),
          isFalse);
    });
  }, onPlatform: {
    'windows': Skip('These tests do not work on Windows (#2446)')
  });

// TODO(srawlins): More unit tests: @example with `config.examplePathPrefix`,
// @tool.
}

class _HasWarning extends Matcher {
  final PackageWarning kind;

  final String message;

  _HasWarning(this.kind, this.message);

  @override
  bool matches(dynamic actual, Map<Object, Object> matchState) {
    if (actual is ModelElement) {
      return actual.packageGraph.packageWarningCounter
          .hasWarning(actual, kind, message);
    } else {
      return false;
    }
  }

  @override
  Description describe(Description description) =>
      description.add('Library to be warned with $kind and message:\n$message');

  @override
  Description describeMismatch(dynamic actual, Description mismatchDescription,
      Map<Object, Object> matchState, bool verbose) {
    if (actual is ModelElement) {
      var warnings = actual
          .packageGraph.packageWarningCounter.countedWarnings[actual.element];
      return mismatchDescription.add('has warnings: $warnings');
    } else {
      return mismatchDescription.add('is a ${actual.runtimeType}');
    }
  }
}
